4.2. 참조와 대여
참조와 대여
매번 전달한 값을 돌려주는 것은 번거롭다.
이대신 참조자라 하여 포인터와 같은 것을 통해 데이터를 대여하는 방법이 유효하다.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
이렇게 &을 써서 데이터를 잠시만 빌려준다는 의미를 넣어준다.
이는 소유권을 넘기지 않고 값을 참조만 하겠다는 것을 뜻한다.
나중에 배우겠지만 *라고 하여 역참조도 가능하다.
&1은 값을 소유하지 않는 참조자를 생성한다.
이는 사용되지 않을 때까지 버려지지 않는다.
이렇게 참조자를 만드는 행위를 대여라고 부른다.
참조자는 수정될 수 없다.
가변 참조자
참조자는 수정될 수 없지만, mut을 이용해 가변성을 부여할 수 있다.
그러나 가변 참조자는 한번만 사용될 수 있다.'
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
위 코드는 에러를 반환한다.
한번이라도 가변 참조자가 생겼다면, 이후에는 해당 값에 대한 참조자를 만들 수 없게 된다.
이러한 설계는 값의 변경에 대한 제어를 원활하게 만든다.
이를 통해 러스트는 컴파일 타임에 데이터 경합(data race)를 방지한다.
- 둘 이상의 포인터가 동시에 같은 데이터에 접근
- 포인터 중 하나 이상이 쓰기 작업 진행
- 데이터 접근 동기화 매커니즘의 부재
위의 상황들이 데이터 경합의 상황이며, 이는 정의되지 않은 동작을 일으킨다.
그럼에도 가변 참조를 중첩하고 싶다면, 스코프 블록을 만들면 된다.
유의할 점은, 참조가 한번이라도 일어난 블록 내에서 추가적으로 가변 참조자를 만드는 것은 애초에 불가능하다는 것이다.
let mut s = String::from("hello");
let r1 = &s; // 문제없음
let r2 = &s; // 문제없음
let r3 = &mut s; // 큰 문제
println!("{}, {}, and {}", r1, r2, r3);
불변 참조자 입장에서 가변 참조자의 영향을 받을 상황을 컴파일 단계에서 막아버리는 것이다.
반대로 불변 참조자를 여러 개 만드는 것은 가능하다.
let mut s = String::from("hello");
let r1 = &s; // 문제없음
let r2 = &s; // 문제없음
println!("{} and {}", r1, r2);
// 이 지점 이후로 변수 r1과 r2는 사용되지 않습니다
let r3 = &mut s; // 문제없음
println!("{}", r3);
위 코드에서는 참조된 값이 사용되어 버려졌기 때문에 가변 참조자를 만드는 것이 가능했다.
이러한 방식은 원하지 않는 동작이 일어나는 것을 원천적으로 차단한다.
댕글링 참조
dangling pointer는 포인터가 남아있는 상황에서 가리키는 메모리를 일부 해제하게 되어 소유권이 명확하지 않은 메모리를 참조하고 있는 포인터를 말한다.
fn dangle() -> &String { // dangle은 String의 참조자를 반환합니다
let s = String::from("hello"); // s는 새로운 String입니다
&s // String s의 참조자를 반환합니다
} // 여기서 s는 스코프 밖으로 벗어나고 버려집니다. 해당 메모리는 해제됩니다.
// 위험합니다!
주석을 볼 수 있듯이 s는 스코프로 나갈 때 버려지는데 이에 대한 참조자가 반환되기에 에러를 발생시킨다.
이러한 함수가 존재하는 것만으로도(사용되지 않더라도) 러스트에서는 컴파일이 되지 않는다.
이런 식으로 사용을 하고 싶다면, 그냥 참조자가 아니라 해당 값을 그대로 반환하는 식으로 코드를 작성하라.
결국 참조자란?
- 하나의 가변 참조자만 있거나, 복수의 불변 참조자
- 참조자는 항상 유효해야 함